<?php
/**
 *
 * @author    Lucian Daia < lucian.daia@live.com >
 * @copyright 2010, Lucian Daia
 * @package   KChunk
 */
class KChunk_Application
{
	/**
	 * URL to the target file
	 * @var string
	 */
	protected $_downloadUrl = null;

	/**
	 * Name of the local file the resource will be saved to
	 * @var string
	 */
	protected $_dataFile = null;

	/**
	 * The maximum size of each chunk to download, in bytes
	 * @var int
	 */
	protected $_chunkSize = 1024;

	/**
	 * Maximum numbers of workers to start in order to complete the tasks
	 * @var int
	 */
	protected $_maxWorkers = 10;

	/**
	 * Path to the SQLite data source file
	 * @var string
	 */
	protected $_dataSourcePath = './kchunk_db.sqlite';

	/**
	 * @var KChunk_Datasource
	 */
	protected $_dataSource = null;

	/**
	 * Whether to stop after a number of bytes
	 * @var int
	 */
	protected $_limitBytes = null;

	/**
	 * Constructor
	 * @param string $downloadUrl
	 * @param string $dataFile
	 */
	public function __construct($downloadUrl, $dataFile = null)
	{
		$this->setUrl($downloadUrl);

		if ($dataFile) {
			$this->setDataFile($dataFile);
		} else {
			$this->setDataFile(basename($downloadUrl));
		}
	}

	/**
	 * @param string $downloadUrl
	 */
	public function setUrl($downloadUrl)
	{
		if (!$downloadUrl) {
			throw new Exception('The download URI must be provided');
		}

		if (!Zend_Uri::check($downloadUrl)) {
			throw new Exception('URI is malformed: ' . $downloadUrl);
		}

		$this->_downloadUrl = $downloadUrl;
		return $this;
	}

	/**
	 * @return string
	 */
	public function getUrl()
	{
		return $this->_downloadUrl;
	}

	/**
	 * @param string $fileName
	 */
	public function setDataFile($fileName)
	{
		$this->_dataFile = $fileName;
	}

	/**
	 * @return string
	 */
	public function getDataFile()
	{
		return $this->_dataFile;
	}

	/**
	 * @param int $chunkSize
	 */
	public function setChunkSize($chunkSize)
	{
		$this->_chunkSize = intval($chunkSize);
		return $this;
	}

	/**
	 * @return int
	 */
	public function getChunkSize()
	{
		return $this->_chunkSize;
	}

	/**
	 * @param int $maxWorkers
	 */
	public function setMaxWorkers($maxWorkers)
	{
		$this->_maxWorkers = intval($maxWorkers);
		return $this;
	}

	/**
	 * @return int
	 */
	public function getMaxWorkers()
	{
		return $this->_maxWorkers;
	}

	/**
	 * @param string $dsPath
	 */
	public function setDatasourcePath($dsPath)
	{
		$this->_dataSourcePath = $dsPath;
		return $this;
	}

	/**
	 * @return string
	 */
	public function getDatasourcePath()
	{
		return $this->_dataSourcePath;
	}

	/**
	 * @param int $limit
	 */
	public function setDownloadLimit($limit)
	{
		$this->_limitBytes = intval($limit);
		return $this;
	}

	/**
	 * @return int
	 */
	public function getDownloadLimit()
	{
		return $this->_limitBytes;
	}

	/**
	 * if the data source doesn't exist, create it and all the tables needed
	 */
	public function initDatasource()
	{
		$dataSourcePath = $this->getDatasourcePath();

		if (!file_exists($dataSourcePath)) {
			$this->_dataSource = new KChunk_Datasource($dataSourcePath);
			$this->_dataSource->createDatabaseStructure();
		} else {
			$this->_dataSource = new KChunk_Datasource($dataSourcePath);
		}
	}

	/**
	 * check if the file has a download plan
	 */
	public function initDownloadPlan()
	{
		if (!$this->_dataSource instanceof KChunk_Datasource) {
			throw new Exception('Datasource is not initalized properly.');
		}

		if (!$this->_dataSource->downloadPlanExists($this->getUrl())) {
			// extract resource size
			$resource      = new KChunk_HttpProbe($this->getUrl());
			$contentLength = $resource->getResourceContentLength();

			$this->_dataSource->createDownloadPlan(
				$this->getUrl(), $contentLength,
				$this->getDataFile(), $this->getChunkSize());

			// create a placeholder file, the size of the resource, in which all
			// workers will write segments
			echo 'Created download plan for ' . $this->getUrl() . PHP_EOL;
			KChunk_Filesystem::createBlankFile($this->getDataFile(), $contentLength);
		} else {
			echo 'Resuming download of: ' . $this->getUrl() . PHP_EOL;
			$this->setDataFile($this->_dataSource->getTargetFilename($this->getUrl()));
		}
	}

	public function run()
	{
		// todo: check if data file exists. If not, it's useless to check for
		// a download plan, since there is no placeholder.
		// How to solve: recreate the download plan

		//$workerPool = new KChunk_WorkerPool($url, $contentLength, CHUNK_SIZE, MAX_WORKERS);
		$handles = array();

		$start = microtime(true);
		// see how many workers are needed
		do {
			$done = false;

			for ($i = 0; $i < $this->getMaxWorkers(); $i++) {
				// no segments left, so abort
				if (!$segment = $this->_dataSource->getNextSegment($this->getUrl())) {
					$done = true;
					break;
				}

				// if these is a download limit, don't go beyond it
				if (($this->_limitBytes != null) &&
					($segment['start_byte'] > $this->_limitBytes)) {
					$done = true;
					break;
				}

				echo 'Starting worker ' . $i . PHP_EOL;
				$command = sprintf('php slave.php %s %s %d %d',
					$this->getUrl(), $this->getDataFile(),
					$segment['start_byte'],
					$segment['start_byte'] + $segment['chunk_size'] - 1);
				$handles[$i] = popen($command, 'r');
				$this->_dataSource->markSegmentAsBegun($this->getUrl(), $segment['start_byte']);
			}

			foreach ($handles as $key => $handle) {
				$read = fread($handles[$key], 2096);
				echo $read . PHP_EOL;
				echo 'Killing worker ' . $key . PHP_EOL;
				unset($handles[$key]);

				if (strpos($read, 'OK') !== false) {
					$parts     = explode(':', $read);
					$startByte = trim($parts[1]);
					$this->_dataSource->removeSegment($this->getUrl(), $startByte);
				}
			}
		} while (!$done);

		echo 'It took: ' . number_format((microtime(true) - $start), 2) . PHP_EOL;
	}

	/**
	 * Performs the necessary cleanup for the application termination
	 */
	public function cleanup()
	{
		if ($this->_dataSource instanceof KChunk_Datasource) {
			if ($this->_dataSource->downloadReady($this->getUrl())) {
				$this->_dataSource->removePlan($this->getUrl());
			}
		}
	}
}